/*
Remdator v5.0.0
http://code.google.com/p/remdator/

Copyright (c) 2012 Marc Tanti (www.marctanti.com)

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

;(function ($, undefined) {
if (typeof($.remdator) == "undefined")
{
	$.remdator = {};
}

function RadioTableFormElement($args)
//Radio table DOM wrapper.
//$args: {
//	dom: A jQuery object wrapping the <table> tag to represent. Table must have a <tbody> element with one row. The row in the <tbody> tag will be removed and all of its attributes (and td vs th tags) will be used in every new row. If it has a <thead> or <tfoot> row then they will be preserved. The first column will be used for storing radio buttons only. Feel free to use "display:none" on any columns that are there for internal use only.
//	radioName: The name attribute of the radio buttons.
//	fieldNames: [ A list of identifier names to associate with each column respectively. These are used only to refer to them in radio table methods. The first column is used to store the radio button only and is nameless. ]
//	scrollableParent: The parent container of this radio table that should be scrolled to automatically display a selected item. To disable auto scrolling leave undefined and to scroll whole page set to $(window).
//  defaultAjaxOperation: { Default operation parameters to override missing parameters in ajaxOperations.
//		errorChecker($ajaxOutputArgs): The function which checks the output from the remote script to see if it returned an error and throws a RemoteError exception if so.
//			$ajaxOutputArgs: {
//				source: The DataElement object that the operation belongs to.
//				requestData: The jQuery ajax data used to make the ajax request.
//				output: The remote script output.
//			}
//		success($source, $requestData, $output): The function which does something with the output from the remote script if no errors were returned.
//			$ajaxOutputArgs: {
//				source: The DataElement object that the operation belongs to.
//				requestData: The jQuery ajax data used to make the ajax request.
//				output: The remote script output.
//			}
//		settings: Default jQuery ajax settings as specified in http://api.jQuery.com/jQuery.ajax/ (which overrides jQuery defaults) to use when calling a remote script (you cannot override url, data, success or error). If undefined, will use jQuery defaults.
//	}
//	ajaxOperations: { An object where each value, which represents an operation of your choosing, maps to an ajax operation object, creating a new ajax operation.
//		operation name : {
//			url: The url of a remote script to call through ajax for loading or changing remote data.
//			errorChecker($ajaxOutputArgs): The function which checks the output from the remote script to see if it returned an error and throws a RemoteError exception if so.
//				$ajaxOutputArgs: {
//					source: The DataElement object that the operation belongs to.
//					requestData: The jQuery ajax data used to make the ajax request.
//					output: The remote script output.
//				}
//			success($source, $requestData, $output): The function which does something with the output from the remote script if no errors were returned.
//				$ajaxOutputArgs: {
//					source: The DataElement object that the operation belongs to.
//					requestData: The jQuery ajax data used to make the ajax request.
//					output: The remote script output.
//				}
//			settings: Custom jQuery ajax settings as specified in http://api.jQuery.com/jQuery.ajax/ (which extends and overrides defaultAjaxOperation) to use when calling the remote script (you cannot override url, data, success or error).
//		}
//	}
//	defaultFormOperation: { Default operation parameters to override missing parameters in formOperations.
//		errorChecker($formOutputArgs): The function which checks the output from the remote script to see if it returned an error and throws a RemoteError exception if so.
//			$formOutputArgs: {
//				source: The DataElement object that the operation belongs to.
//				form: The form used to submit the request.
//				output: The remote script output.
//			}
//		success($formOutputArgs): The function which does something with the output from the remote script if no errors were returns.
//			$formOutputArgs: {
//				source: The DataElement object that the operation belongs to.
//				form: The form used to submit the request.
//				output: The remote script output.
//			}
//		settings: { Default settings for submitting a form.
//			form: The jQuery wrapper object of the form to submit. Must be available at the time of calling this function.
//			iframe: The iframe to recieve the script's output in. Must be available at the time of calling this function and must have a unique "name" attribute.
//			dataType: The data type (which overrides defaultFormDataType) of the output of the remote script after submitting the form. Can be either "json", "xml" or "text". Default is "text".
//			preSubmit($preSubmitArgs): The function to call immediately before submitting the form (such as to fill in hidden fields).
//				$preSubmitArgs: {
//					$source: The DataElement object that the operation belongs to.
//					$form: The form that will be used to submit the request.
//				}
//		}
//	}
//	formOperations: { An object where each value, which represents an operation of your choosing, maps to a form operation object, creating a new form submission operation.
//		operation name : {
//			url: The url of the remote script to submit the form to.
//			errorChecker($formOutputArgs): The function which checks the output from the remote script to see if it returned an error and throws a RemoteError exception if so.
//				$formOutputArgs: {
//					source: The DataElement object that the operation belongs to.
//					form: The form used to submit the request.
//					output: The remote script output.
//				}
//			success($formOutputArgs): The function which does something with the output from the remote script if no errors were returns.
//				$formOutputArgs: {
//					source: The DataElement object that the operation belongs to.
//					form: The form used to submit the request.
//					output: The remote script output.
//				}
//			settings: { Settings for submitting the form for this operation (which override defaultFormOperation).
//				form: The jQuery wrapper object of the form to submit. Must be available at the time of calling this function.
//				iframe: The iframe to recieve the script's output in. Must be available at the time of calling this function and must have a unique "name" attribute.
//				dataType: The data type of the output of the remote script after submitting the form. Can be either "json", "xml" or "text". Default is "text".
//				preSubmit($preSubmitArgs): The function to call immediately before submitting the form (such as to fill in hidden fields).
//					$preSubmitArgs: {
//						$source: The DataElement object that the operation belongs to.
//						$form: The form that will be used to submit the request.
//					}
//			}
//		}
//	}
//}
{
	if (typeof($args) != "undefined")
	{
		//Inherit from CollectionDataElement.
		$.remdator.CollectionDataElement.call(this, $.extend({}, $args, { initLoaded : false }));
		
		$.remdator.assertArgType({ arg : $args.dom, name : "dom", type : "object" });
		$.remdator.assertArgType({ arg : $args.fieldNames, name : "fieldNames", type : "object" });
		$.remdator.assertArgType({ arg : $args.radioName, name : "radioName", type : "string" });
		$.remdator.assertArgType({ arg : $args.scrollableParent, name : "scrollableParent", type : "object", orUndefined : true });
		
		if (!$args.dom.is("table"))
		{
			throw new $.remdator.GeneralError({ message : "Incorrect DOM object." });
		}
		if ($args.dom.children("tr").length > 0)
		{
			throw new $.remdator.GeneralError({ message : "Table must have all rows in a <tbody>, <thead> or <tfoot> element." });
		}
		if ($args.dom.children("tbody").length != 1)
		{
			throw new $.remdator.GeneralError({ message : "Table must have one <tbody> element." });
		}
		if ($args.dom.children("tbody").children("tr").length != 1)
		{
			throw new $.remdator.GeneralError({ message : "Table must have one <tbody> element with one <tr> element." });
		}
		if ($args.dom.children("tbody").children("tr").children("td,th").length == 0)
		{
			throw new $.remdator.GeneralError({ message : "Table must have one <tbody> element with one <tr> element which has at least one <td> or <th> element." });
		}
		
		var $this = this;
		
		//The jQuery object wrapping the DOM element.
		this._dom = $args.dom;
		
		//Add an inversed reference to this object in the DOM element.
		this._dom.data("remdator", this);
		
		//Radio button name.
		this._radioName = $args.radioName;
		
		//Field names.
		if ($args.fieldNames.length != $args.dom.children("tbody").children("tr").children("td,th").length - 1)
		{
			throw new $.remdator.GeneralError({ message : "The length of the fieldNames argument must be one more than the number of <td> and <th> elements in the <tbody> element." });
		}
		this._columns = [];
		//Radio button field
		this._columns.push({
			name : "",
			attr : this._dom.children("tbody").children("tr").children("td,th").first().attributes(),
			tag  : this._dom.children("tbody").children("tr").children("td,th").first().prop("tagName")
		});
		//Other fields
		for (var $i = 0; $i < $args.fieldNames.length; $i++)
		{
			$.remdator.assertArgType({ arg : $args.fieldNames[$i], name : "fieldNames["+$i+"]", type : "string" });
			
			this._columns.push(
				{
					name : $args.fieldNames[$i],
					attr : this._dom.children("tbody").children("tr").children("td,th").eq($i + 1).attributes(),
					tag  : this._dom.children("tbody").children("tr").children("td,th").eq($i + 1).prop("tagName").toLowerCase(),
				}
			);
		}
		
		this._dom.children("tbody").empty();
		
		//As soon as an item is selected, make sure that it is visible.
		this._scrollableParent = undefined;
		if (typeof($args.scrollableParent) != "undefined")
		{
			this._scrollableParent = $args.scrollableParent;
			this.addChangeListener({ listener : function($eventArgs) {
				if (!$this.isItemInScrollView({ value : "" }))
				{
					$this.scrollToItem({ value : "" });
				}
			} });
		}
	}
}
RadioTableFormElement.prototype = new $.remdator.CollectionDataElement;

/***********************************/
RadioTableFormElement.prototype._getItems = function()
//Get an array or jQuery object with all the items in the collection.
{
	$.remdator.CollectionDataElement.prototype._getItems.call(this);
	
	return this._dom.children("tbody").find("input[type=radio][name=" + this._radioName + "]");
}

/***********************************/
RadioTableFormElement.prototype._getItem = function($args)
//Get a particular item with a particular value from the collection. (Place holder)
//$args: {
//	value: The value of the item of interest. If empty string, will get the currently selected item (the one whose value is returned by getValue()).
//}
{
	$.remdator.CollectionDataElement.prototype._getItem.call(this, $args);

	if ($args.value == "")
	{
		return this._getItems().filter(":checked");
	}
	else
	{
		return this._getItems().filter("[value=" + $args.value + "]");
	}
}

/***********************************/
RadioTableFormElement.prototype.getDOM = function()
//Get the jQuery object wrapping the form element.
{
	return this._dom;
};

/***********************************/
RadioTableFormElement.prototype.scrollToItem = function($args)
//Scrolls to make an item at the top.
//$args: {
//	scrollableParent: The parent container of this radio table which should be scrolled. To scroll whole page set to $(window). Leave undefined to use scrollableParent set in constructor.
//	value: The value of the item to scroll to. Empty string scrolls to the selected item.
//}
{
	$.remdator.assertArgType({ arg : $args, name : "arguments", type : "object" });
	$.remdator.assertArgType({ arg : $args.scrollableParent, name : "scrollableParent", type : "object", orUndefined : true });
	$.remdator.assertArgPresent({ arg : $args.value, name : "value" });
	
	var $scrollableParent = $args.scrollableParent;
	if (typeof($args.scrollableParent) == "undefined")
	{
		if (typeof(this._scrollableParent) == "undefined")
		{
			throw new ArgTypeError({ message : "scrollableParent cannot be undefined since scrollableParent in the constructor is also undefined.", arg : $args.scrollableParent });
		}
		$scrollableParent = this._scrollableParent;
	}
	
	var $itemRow = this._getItem({ value : $args.value }).parent();
	var $itemRowTop = $itemRow.offset().top;
	
	//Scroll whole page
	if ($scrollableParent.is($(window)))
	{
		$scrollableParent.scrollTop($itemRowTop - $scrollableParent.height()/2 + $itemRow.outerHeight()/2);
	}
	//Scroll container
	else
	{
		var $border = parseInt($scrollableParent.css("border-top"));
		var $containerTop = $scrollableParent.offset().top + $border;
		var $containerScrollTop = $scrollableParent.scrollTop();
		$scrollableParent.scrollTop($containerScrollTop + ($itemRowTop - $containerTop - $scrollableParent.innerHeight()/2 + $itemRow.outerHeight()/2));
	}
}

/***********************************/
RadioTableFormElement.prototype.isItemInScrollView = function($args)
//Checks if an item is in view due to scrolling.
//$args: {
//	scrollableParent: The parent container of this radio table which is scrolled. If whole page is scrolled, set to $(window). Leave undefined to use scrollableParent set in constructor.
//	value: The value of the item to check. Empty string checks the selected item.
//}
{
	$.remdator.assertArgType({ arg : $args, name : "arguments", type : "object" });
	$.remdator.assertArgType({ arg : $args.scrollableParent, name : "scrollableParent", type : "object", orUndefined : true });
	$.remdator.assertArgPresent({ arg : $args.value, name : "value" });
	
	var $scrollableParent = $args.scrollableParent;
	if (typeof($args.scrollableParent) == "undefined")
	{
		if (typeof(this._scrollableParent) == "undefined")
		{
			throw new ArgTypeError({ message : "scrollableParent cannot be undefined since scrollableParent in the constructor is also undefined.", arg : $args.scrollableParent });
		}
		$scrollableParent = this._scrollableParent;
	}
	
	var $itemRow = this._getItem({ value : $args.value }).parent();
	var $itemRowTop = $itemRow.offset().top;
	var $itemRowBottom = $itemRowTop + $itemRow.outerHeight();
	
	var $containerTop = 0;
	var $containerBottom = 0;
	
	//Scrollable parent is whole page
	if ($scrollableParent.is($(window)))
	{
		$containerTop = $scrollableParent.scrollTop();
		$containerBottom = $containerTop + $scrollableParent.height();
	}
	//Scroll parent is a container
	else
	{
		$border = parseInt($scrollableParent.css("border-top"));
		$containerTop = $scrollableParent.offset().top + $border;
		$containerBottom = $containerTop + $scrollableParent.innerHeight();
	}
	
	return $itemRowTop >= $containerTop && $itemRowBottom <= $containerBottom;
}

/***********************************/
RadioTableFormElement.prototype.getValue = function()
//Get the current value.
{
	$.remdator.CollectionDataElement.prototype.getValue.call(this);
	
	return this._getItem({ value : "" }).attr("value");
};

/***********************************/
RadioTableFormElement.prototype.getAll = function()
//Get an object mapping every value to its corresponding data.
{
	$.remdator.CollectionDataElement.prototype.getAll.call(this);
	
	var $this = this;
	
	var $result = {};
	$.each(this._getItems(), function($i, $e) {
		var $value = $($e).attr("value")
		$result[$value] = $this.getContents({ value : $value });
	});
	
	return $result;
}

/***********************************/
RadioTableFormElement.prototype.getContents = function($args)
//Get an array of the contents of a particular row.
//$args: {
//	value: The value of the radio button in the row of interest.
//}
{
	$.remdator.assertArgType({ arg : $args, name : "arguments", type : "object" });
	$.remdator.assertArgPresent({ arg : $args.value, name : "value" });
	
	if (!this.isItemExist($args))
	{
		throw new $.remdator.GeneralError({ message : "Attempting to get the contents of a non-existent row." });
	}
	
	var $tds = this._getItem($args).parent().parent().find("td,th");
	var $result = {};
	for (var $i = 1; $i < this._columns.length; $i++)
	{
		$result[this._columns[$i].name] = $tds.eq($i).contents();
	}
	return $result;
};

/***********************************/
RadioTableFormElement.prototype.setValue = function($args)
//Set the current value.
//$args: {
//	value: The value to set.
//}
{
	$.remdator.CollectionDataElement.prototype.setValue.call(this, $args);
	
	this._getItem($args).attr("checked", true);
	this._loaded = true;
	
	this.fireChangeEvent();
};

/***********************************/
RadioTableFormElement.prototype.isItemExist = function($args)
//Check if the item with a particular value exists.
//$args: {
//	value: The value of the item of interest. If empty string, will check if there are any items at all.
//}
{
	$.remdator.CollectionDataElement.prototype.isItemExist.call(this, $args);
	
	return this._getItem($args).length != 0;
}

/***********************************/
RadioTableFormElement.prototype.clearItems = function()
//Remove all items.
{
	$.remdator.CollectionDataElement.prototype.clearItems.call(this);

	this._loaded = false;
	this._dom.children("tbody").empty();
};

/***********************************/
RadioTableFormElement.prototype.__noEventAddItem = function($args)
//Add a new item with no events fired. Method is common to loadItems and addItem.
//$args: {
//	value: The value of the new item.
//	contents: An object containing keys which are the column names and values which are the contents in the corresponding field.
//	isHTML: [
//		A list of keys in the contents that should be interpreted as HTML rather than plain text.
//	]
//}
{
	$.remdator.assertArgType({ arg : $args, name : "arguments", type : "object" });
	$.remdator.assertArgPresent({ arg : $args.value, name : "value" });
	$.remdator.assertArgType({ arg : $args.contents, name : "contents", type : "object" });
	$.remdator.assertArgType({ arg : $args.isHTML, name : "isHTML", type : "object", orUndefined : true });
	
	var $this = this;
	
	for (var $i = 1; $i < this._columns.length; $i++)
	{
		if (typeof($args.contents[this._columns[$i].name]) == "undefined")
		{
			throw new $.remdator.GeneralError({ message : "Missing field in contents." });
		}
	}
	for (var $name in $args.contents)
	{
		var $found = false;
		for (var $i = 1; $i < this._columns.length; $i++)
		{
			if (this._columns[$i].name == $name)
			{
				$found = true;
				break;
			}
		}
		if (!$found)
		{
			throw new $.remdator.GeneralError({ message : "Non-existent field in contents." });
		}
	}
	
	var isHTML = {};
	if (typeof($args.isHTML) != "undefined")
	{
		for (var $i = 0; $i < $args.isHTML.length; $i++)
		{
			$.remdator.assertArgType({ arg : $args.isHTML[$i], name : "isHTML["+$i+"]", type : "string" });
			
			var $found = false;
			for (var $j = 0; $j < this._columns.length; $j++)
			{
				if ($args.isHTML[$i] == this._columns[$j].name)
				{
					$found = true;
					break;
				}
			}
			if (!$found)
			{
				throw new $.remdator.GeneralError({ message : "Element '"+$args.isHTML[$i]+"' in isHTML is not an existent column name." });
			}
			else
			{
				isHTML[$args.isHTML[$i]] = true;
			}
		}
	}

	var $tr = $("<tr></tr>");
	var $td = null;
	
	//Radio button field
	$td = $("<"+this._columns[0].tag+"></"+this._columns[0].tag+">")
		.attr(this._columns[0].attr)
		.append(
			$("<input />")
				.attr("type", "radio")
				.attr("name", this._radioName)
				.attr("value", $args.value)
				.change(function(){ $this.fireChangeEvent(); })
		);
	$tr.append($td);
	
	//Other fields
	for (var $i = 1; $i < this._columns.length; $i++)
	{
		$td = $("<"+this._columns[$i].tag+"></"+this._columns[$i].tag+">")
			.attr(this._columns[$i].attr)
			.safeAppend(
				$args.contents[this._columns[$i].name],
				isHTML[this._columns[$i].name] ? true : false
			)
		$tr.append($td);
	}
	
	this._dom.children("tbody").append($tr);
}

/***********************************/
RadioTableFormElement.prototype.loadItems = function($args)
//Clear list of items and load a new list.
//$args: {
//	items: [
//		{
//			value: The value of the new item.
//			select: A boolean value indicating whether to select the new item. Only the last item in the list whose value is true will be select.
//			contents: An object containing keys which are the column names and values which are the contents in the corresponding field.
//			isHTML: [
//				A list of keys in the contents that should be interpreted as HTML rather than plain text.
//			]
//		}
//	],
//	smartSelect : A boolean value indicating whether to select a value based on clues indicating whether the new list is the same as the old but with an addition, deletion or update. Done by modifying the items argument. If undefined then it will be false. If an item in the items argument is set to be selected then the reselectSame argument will be ignored.
//}
{
	$.remdator.CollectionDataElement.prototype.loadItems.call(this, $args);
	
	for (var $i = 0; $i < $args.items.length; $i++)
	{
		$.remdator.assertArgPresent({ arg : $args.items[$i].contents, name : "items["+$i+"].contents" });
		if (typeof($args.items[$i].contents[""]) != "undefined")
		{
			throw new $.remdator.GeneralError({ message : "Attempting to set the radio button field in items["+$i+"].contents." });
		}
	}
	
	this.clearItems();
	
	var $lastSelected = 0;
	for (var $i = 0; $i < $args.items.length; $i++)
	{
		this.__noEventAddItem($args.items[$i]);
		if ($args.items[$i].select)
		{
			$lastSelected = $i;
		}
	}
	
	if ($args.items.length > 0)
	{
		this.setValue({ value : $args.items[$lastSelected].value });
	}
};

/***********************************/
RadioTableFormElement.prototype.addItem = function($args)
//Add a new item.
//$args: {
//	value: The value of the new item.
//	select: A boolean value indicating whether to select the newly added item.
//	contents: An object containing keys which are the column names and values which are the contents in the corresponding field.
//	isHTML: [
//		A list of keys in the contents that should be interpreted as HTML rather than plain text.
//	]
//}
{
	$.remdator.CollectionDataElement.prototype.addItem.call(this, $args);
	
	$.remdator.assertArgPresent({ arg : $args.contents, name : "contents" });
	
	if (typeof($args.contents[""]) != "undefined")
	{
		throw new $.remdator.GeneralError({ message : "Attempting to set the radio button field." });
	}
	
	this.__noEventAddItem($args);
	
	if ($args.select || this.numItems() == 1)
	{
		this.setValue({ value : $args.value });
	}
};

/***********************************/
RadioTableFormElement.prototype.updateItem = function($args)
//Update an item.
//$args: {
//	value: The value of the item to update. If empty string, will update the currently selected item.
//	contents: An object containing keys which are the column names and values which are the new contents to use in the corresponding field.
//	isHTML: Whether the contents are HTML or just text.
//}
{
	$.remdator.CollectionDataElement.prototype.updateItem.call(this, $args);
	
	$.remdator.assertArgPresent({ arg : $args.contents, name : "contents" });
	
	if (typeof($args.contents[""]) != "undefined")
	{
		throw new $.remdator.GeneralError({ message : "Attempting to set the radio button field." });
	}
	
	var $tds = this._getItem($args).parent().parent().find("td,th");
	for (var $i = 1; $i < this._columns.length; $i++)
	{
		var $newContent = $args.contents[this._columns[$i].name];
		if (typeof($newContent) != "undefined")
		{
			$tds.eq($i).empty();
			$tds.eq($i).safeAppend($newContent, $args.isHTML);
		}
	};
};

/***********************************/
RadioTableFormElement.prototype.deleteItem = function($args)
//Delete an item.
//$args: {
//	value: The value of the item to delete. If empty string, will delete the currently selected item.
//}
{
	$.remdator.CollectionDataElement.prototype.deleteItem.call(this, $args);
	
	var $selectedValue = this.getValue();
	var $selectedIndex = this._getItems().index(this._getItem({ value : "" }));
	
	this._getItem($args).parent().parent().remove();
	
	if ($args.value == "" || $selectedValue == $args.value)
	{
		if (this.isEmpty())
		{
			this._loaded = false;
		}
		else
		{
			var $newIndex = $selectedIndex == 0 ? 0 : $selectedIndex - 1;
			this.setValue({ value : this._getItems().eq($newIndex).attr("value") });
		}
	}
};

//Allow user to give a particular radio table remdator functionalities by calling a method from the radio table.
//For example $("radio table selector").remdator_radiotable({args})
//The RadioTableFormElement object will be returned when calling the remdator_radiotable method.
$.fn.remdator_radiotable = function($args)
{
	return new RadioTableFormElement($.extend({}, $args, { dom : this }));
};
//Add to remdator namespace in jQuery.
$.remdator.RadioTableFormElement = RadioTableFormElement;
})(jQuery);